Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: SwiftUI View Rendering Instrumentation #20

Merged
merged 16 commits into from
Nov 18, 2024

Conversation

MustafaHaddara
Copy link
Contributor

@MustafaHaddara MustafaHaddara commented Oct 30, 2024

Which problem is this PR solving?

  • Closes #

Short description of the changes

SwiftUI rendering is very interesting!

This PR currently has a whole bunch of instrumentation and logging which makes it easier to see what's going on

This PR intentionally does not ship a view modifier form of the instrumentation. This is because ViewModifiers receive a Content struct (vs. Views that receive an @ViewBuilder () -> Content callback!); that is: ViewModifiers receive a view after it has been initialized, and therefore have no way to retroactively start a span before the view begins initializing.

Compare the spans/log output from using the wrapper view form:

image

with the view modifier form:
image

And, now that we've settled on the wrapper form, here's the kinds of traces we're producing:

image

How to verify that this has the expected result

Copy link
Collaborator

@beekhc beekhc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some early feedback.

Examples/SmokeTest/SmokeTest/ContentView.swift Outdated Show resolved Hide resolved
Examples/SmokeTest/SmokeTest/ContentView.swift Outdated Show resolved Hide resolved
Sources/Honeycomb/HoneycombInstrumentedView.swift Outdated Show resolved Hide resolved
Sources/Honeycomb/HoneycombInstrumentedView.swift Outdated Show resolved Hide resolved
@MustafaHaddara MustafaHaddara marked this pull request as ready for review November 13, 2024 21:30
@MustafaHaddara MustafaHaddara requested a review from a team as a code owner November 13, 2024 21:30
@MustafaHaddara MustafaHaddara changed the title feat: [WIP] view rendering instrumentation feat: view rendering instrumentation Nov 13, 2024
@MustafaHaddara MustafaHaddara changed the title feat: view rendering instrumentation feat: SwiftUI View Rendering Instrumentation Nov 13, 2024
@MustafaHaddara MustafaHaddara added type: enhancement New feature or request version: bump minor A PR that adds behavior, but is backwards-compatible. labels Nov 13, 2024
Copy link
Collaborator

@beekhc beekhc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, this looks great, and I just have a bunch of nits. But also, are you going to add some SmokeTest UI tests and assertions?


ViewInstrumentationView()
.padding()
.tabItem { Label("View Instrumentation", systemImage: "ruler") }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's really hard to resist the urge to bikeshed what icon to use for each tab 😆

} onEditingChanged: { editing in
isEditing = editing
if !editing {
delay = sliderDelay
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really thought that since the value of the slider was a $ binding that you would just be able to have Slider(value: $delay, and not have any onEditingChanged: handler. If this is necessary, could you add a comment explaining why we need all three state variables?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah the idea was that we'd be able to adjust the slider but the rest of the view wouldn't re-render until you stopped editing it.

BUT clearly I wasn't paying enough attention and that wasn't actually working. I'll restructure it and add a comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

works now:

Screen.Recording.2024-11-15.at.2.48.07.PM.mp4

(yes i know the simulator has a screen recorder but i wanted to capture my mouse cursor lol)


private func timeConsumingCalculation(_ delay: Double) -> String {
print("starting time consuming calculation")
sleep(UInt32(delay))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sleep() is the POSIX function that takes a whole number of seconds, but it would be better to use the Swift function that takes a TimeInterval: Thread.sleep(forTimeInterval: delay)

}
}

private func timeConsumingCalculation(_ delay: Double) -> String {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than Double, it would be more idiomatic and self-documenting to use TimeInterval, which is an alias for Double. I would try to use it as much as possible. Like, if we still need two state variables above, probably at least the delay one should be TimeInterval too.

}

var body: some View {
print("\(name) body started rendering")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't forget to remove print statements.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any kind of debug logging infrastructure? I've found these logs to be helpful when I play with the smoke tests.


print("\(name) content appeared")
span.setAttribute(key: "TotalDuration", value: Date().timeIntervalSince(initTime))
span.end(time: Date())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be better to capture the Date() once and use the same value on both these lines?

print("\(name) content appeared")
span.setAttribute(key: "TotalDuration", value: Date().timeIntervalSince(initTime))
span.end(time: Date())
bodySpan.end(time: endTime)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little confused about this. Is there a reason we can't do this on line 40, right after endTime is set?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted it to remain active in context, I put some comments in the code that should it explain it better.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the comment. very helpful.

@@ -20,7 +20,7 @@ span_attributes_for() {
# $3 - attribute key
# $4 - attribute type
attribute_for_span_key() {
attributes_from_span_named $1 $2 | \
attributes_from_span_named "$1" "$2" | \
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're emitting spans named View Render so these args need to be quoted for our utils to work correctly.

Copy link
Collaborator

@beekhc beekhc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great. I made a few comments for future discussion, but they shouldn't prevent us from submitting this.

README.md Outdated
`View Render` spans encompass the entire rendering process, from initialization to appearing on screen. They include the following attributes:
- `ViewName` (string): the name passed to `HoneycombInstrumentedView`
- `RenderDuration` (double): amount of time to spent initializing the contents of `HoneycombInstrumentedView`
- `TotalDuration` (double): amount of time from when `HoneycombInstrumentedView::body()` is called to when the contents appear on screen
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: it's ., not ::. This isn't C++. :-)

struct DelayedSlider: View {
@State private var sliderDelay = 2.0
@State private var isEditing = false
var delay: Binding<Double>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love it! <3


Specifically, it will emit 2 kinds of span for each view that is wrapped:

`View Render` spans encompass the entire rendering process, from initialization to appearing on screen. They include the following attributes:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm surprised there aren't specific semantic conventions around spaces and capitalization in span names, but it appears there are not: https://github.com/open-telemetry/opentelemetry-specification/blob/v1.37.0/specification/trace/api.md#span

Anyway, this is fine for now, and we can always tweak them later, since we're still in experimental alpha mode. We should probably just be consistent with our web sdk.

print("\(name) content appeared")
span.setAttribute(key: "TotalDuration", value: Date().timeIntervalSince(initTime))
span.end(time: Date())
bodySpan.end(time: endTime)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the comment. very helpful.

@test "Render Instrumentation attributes are correct" {
# we got the spans we expect
result=$(span_names_for "@honeycombio/instrumentation-view" | sort | uniq -c)
assert_equal "$result" ' 7 "View Body"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should keep an eye on this and make sure it isn't flaky. I'm a little worried about something getting rendered an extra time for whatever reason, and the counts being off.

@MustafaHaddara MustafaHaddara merged commit 71410b9 into main Nov 18, 2024
6 checks passed
@MustafaHaddara MustafaHaddara deleted the mh/instrumented-view branch November 18, 2024 20:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: enhancement New feature or request version: bump minor A PR that adds behavior, but is backwards-compatible.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants